CASSINI Spy - Felix John COLIBRI. |
- abstract : a sniffing utility which captures and display all HTTP packets between the Cassini Web Server and your Browser
- key words : ASP.NET development, CASSINI, HTTP sniffer
- software used : Windows XP, Delphi 2006, Delphi 2005
- hardware used : Pentium 2.800Mhz, 512 M memory, 250 G hard disc
- scope : Delphi 8, 2005, 2006, Windows C# framework
- level : Delphi / ASP.NET developer
- plan :
1 - Introduction When debugging ASP.NET applications, it is very helpful to display the content of the information exchanged between the browser (Internet Explorer) and the Web Server (CASSINI or IIS).
ASP.NET can log all kind of information, but the content of what is exchanged between the Client and the Server is not, as far as I know, available.
We first tried to use our own TCP/IP Sniffer. But since CASSINI works on localhost, the packet do not travel up to the Ethernet Driver, where the Packet Sniffer would catch them.
So the alternative we found was to build a specific tool that we will present now.
2 - How it Works A simple descriptioin of Client / Server socket operation is the following:
- the Server (Cassini in our case) loads, opens a Server Socket which starts listening to incoming Client for HTTP requests (port 80):
- "a" Client (Internet Explorer in our case) loads, creates a Client Socket which tries to connect to a port 80 Server (Cassini):
- the Server "accepts" the connection. Since the Server is supposed to handle many Clients, it cannot take care of the communication with all Clients at the same time. This is why is spins-off a "Server Client Socket"
for each incoming Client:
- the Server sends the answer back, in one or several packets, and the
Client happily communicates with his "Server Client Socket":
- the communication is closed down when either client sockets closes (or an error occurs somewhere in the network)
In the case of Cassini / Internet Explorer, the port uses is by default the HTTP port, 80. But Cassini, in its staring dialog, allows to specify the listening port. This will allow the developper to still use the standard 80
port to do bona fide HTTP work on his PC (to Google around or whatever) And similarily, Internet Explorer allows us to specify a connection port in
the address combo box:
Since we can specify both Server and Client ports, we simply place our
sniffing utility in the middle, communicating with Cassini on, say, port 55, and with Internet Explorer on, say, port 81: - Cassini and our spy start listening:
- Internet Explorer starts, and tries to connect to port 81:
- our spy spins off a Server Client Socket which receives the Internet Explorer request:
- our spy creates a Client Socket which will now communicate with Cassini:
- this Client Socket forwards the IE request to Cassini, which spins off a
Server Client Socket :
- the packets then flow between Cassini and Internet Explorer, with our spy
sitting in the middle, capturing the packets and doing whatever it wants with those packets (saving them to disk, formating them and displaying them in a window etc.) :
We showed Internet Explorer, our spy and Cassini on 3 different PCs. In fact, Cassini is by definition a LOCAL Server. It can only handle request
from a browser on the SAME machine. So our figure should represent everything on the same PC. But the idea is the same.
To implement our spy, we simple build a Delphi application with
- a Server Socket listening to Internet Explorer
- the request from Internet Explorer will spin off a Server Client Socket, which will:
- forward the request to Cassini
- watch for Cassini's answers, and shovel them back to Internet Explorer
- all the packets will be displayed, in different formats, in tMemos of our application
3 - Delphi Source Code 3.1 - Socket components It is possible to use the standard Delphi tServerSocket and tClientSocket
components. You may look at our Delphi Socket Architecture paper which explain how to use them, with a sample file transfer project.
We can also use our own Socket components, which were presented in the Simple Web Server paper, or any other Socket library (ICS, Indy, Synapse or whatever).
All we have to do is - override the generic tServer, in order to force him to use our derived Server Client Socket :
- override the generic Server Client Socket, to let it receive IE packets and transfer them the the spy Client Socket :
- override the generic tClientSocket to let him mimic IE and communicate with Cassini :
For this simple utility, we will use our library. The overall UML class diagram is the following: and at the bottom of the figure are our three sockets.
3.2 - The Server Client Socket
Here is the definition our our derived Server Client Socket c_cassini_spy_server_client_socket=
class(c_server_client_socket)
m_on_display_cassini_spy_event: t_po_cassini_spy_display_event;
m_cassini_port: Integer;
m_c_cassini_spy_client_socket: c_cassini_spy_client_socket;
Constructor create_server_client_socket(p_name: String;
p_c_server_socket_ref: c_server_socket;
p_cassini_port: Integer); Override;
procedure handle_can_write_data; Override;
procedure handle_received_data; Override;
procedure handle_remote_client_closed; Override;
procedure handle_received_data_from_cassini(
p_c_cassini_spy_client_socket: c_cassini_spy_client_socket);
Destructor Destroy; Override;
end; // c_cassini_spy_server_client_socket |
where m_c_cassini_spy_client_socket is the Client Socket communicating between the Spy and Cassini There are two interesting methods:
- the event handler which receives packets from Internet Explorer and forwards them to Cassini using the Client Socket:
procedure c_cassini_spy_server_client_socket.handle_received_data;
// -- received some data from the browser
var l_end_of_header_position: Integer;
l_read_index: Integer; begin
// -- fetch the bytes receive_buffered_data;
with m_c_reception_buffer do begin
if Assigned(m_on_display_cassini_spy_event)
then
with m_c_reception_buffer do
m_on_display_cassini_spy_event(1,
f_display_buffer(m_read_index, m_write_index));
// -- set the text to send to Cassini
m_c_cassini_spy_client_socket.m_c_cassini_spy_text_to_send_ref:=
m_c_reception_buffer;
m_c_cassini_spy_client_socket.connect_tee('127.0.0.1', m_cassini_port);
end; // with p_c_byte_buffer end; // handle_received_data
| - the symmetric handler which receives data from Cassini
procedure c_cassini_spy_server_client_socket.handle_received_data_from_cassini(
p_c_cassini_spy_client_socket: c_cassini_spy_client_socket);
// -- now CASSINI did send the answer
procedure send_back(p_c_reception_buffer: c_byte_buffer);
begin with p_c_reception_buffer do
begin // -- the server_client_socket sends back
f_do_send_buffer(@ m_oa_byte_buffer[m_read_index], m_write_index);
// -- move forward
m_read_index:= m_write_index;
end; // with p_c_byte_buffer
end; // send_back begin // handle_received_data_from_cassini
// -- received an answer from Cassini // -- forward to IE
with p_c_cassini_spy_client_socket, m_c_reception_buffer do
if m_write_index> 0
then begin
if Assigned(m_on_display_cassini_spy_event)
then m_on_display_cassini_spy_event(2,
f_display_buffer(m_read_index, m_write_index));
send_back(m_c_reception_buffer);
end; end; // handle_received_data_from_cassini |
3.3 - The Spy Client Socket Here is the definition our our derived Server Client Socket
c_cassini_spy_client_socket=
class(c_client_socket)
m_c_cassini_spy_text_to_send_ref: c_byte_buffer;
m_on_cassini_spy_received_data: t_po_cassini_spy_client_socket_event;
Constructor create_cassini_spy_client_socket(p_name: String);
procedure trace_cassini_spy_client(p_text: String);
procedure connect_to_cassini(p_server: String; p_port: Integer);
procedure handle_connected(p_c_client_socket: c_client_socket);
procedure handle_received_data(p_c_client_socket: c_client_socket);
procedure handle_remote_server_client_socket_closed(
p_c_client_socket: c_client_socket);
Destructor Destroy; Override;
end; // c_cassini_spy_client_socket | and - here is the "connected" handler:
c_cassini_spy_client_socket=
class(c_client_socket)
m_c_cassini_spy_text_to_send_ref: c_byte_buffer;
m_on_cassini_spy_received_data: t_po_cassini_spy_client_socket_event;
Constructor create_cassini_spy_client_socket(p_name: String);
procedure trace_cassini_spy_client(p_text: String);
procedure connect_to_cassini(p_server: String; p_port: Integer);
procedure handle_connected(p_c_client_socket: c_client_socket);
procedure handle_received_data(p_c_client_socket: c_client_socket);
procedure handle_remote_server_client_socket_closed(
p_c_client_socket: c_client_socket);
Destructor Destroy; Override;
end; // c_cassini_spy_client_socket | - and the method sending the data back to IE:
procedure c_cassini_spy_client_socket.handle_received_data(
p_c_client_socket: c_client_socket); // -- fd_read notification was received
var l_received_text: String; begin
// -- notify IE
if Assigned(m_on_cassini_spy_received_data)
then m_on_cassini_spy_received_data(Self);
end; // handle_received_data |
3.4 - The spy Server Socket
Here is the CLASS definition:
t_po_tcp_cassini_spy_event= Procedure(p_c_cassini_spy: c_cassini_spy)
of object; c_cassini_spy=
class(c_basic_object)
m_c_server_socket: c_server_socket;
m_cassini_port: Integer;
m_trace_cassini_spy: Boolean;
m_site_path: String;
m_on_accept_tcp_cassini_spy_socket: t_po_tcp_cassini_spy_event;
m_on_display_cassini_spy_event: t_po_cassini_spy_display_event;
Constructor create_cassini_spy(p_name, p_site_path: String;
p_cassini_port: Integer);
procedure start_cassini_spy(p_ie_port: Integer);
procedure handle_accept(p_c_server_socket: c_server_socket;
p_c_server_client_socket: c_server_client_socket);
procedure close_cassini_spy;
Destructor Destroy; Override;
end; // c_cassini_spy | where: - the constructor builds the server Socket
Constructor c_cassini_spy.create_cassini_spy(p_name, p_site_path: String;
p_cassini_port: Integer); begin
Inherited create_basic_object(p_name);
m_site_path:= p_site_path; m_cassini_port:= p_cassini_port;
// -- specify which c_server_client_sockt Class should be created
m_c_server_socket:= c_server_socket.create_server_socket('server',
c_cassini_spy_server_client_socket);
m_c_server_socket.m_on_after_accept:= handle_accept;
end; // create_cassini_spy | - here the socket starts listening to IE:
procedure c_cassini_spy.start_cassini_spy(p_ie_port: Integer);
begin with m_c_server_socket do
begin m_trace_socket:= m_trace_cassini_spy;
wsa_startup; create_win_socket; wsa_select;
do_bind_win_socket(p_ie_port);
do_listen_to_client(k_default_listen_queue_size)
end; // with m_c_server_socket end; // start_cassini_spy
| - when IE sends data, the Server Socket spins-off a Server Client Socket:
procedure c_cassini_spy.handle_accept(p_c_server_socket: c_server_socket;
p_c_server_client_socket: c_server_client_socket); begin
// -- use the same Memo2 display
with (p_c_server_client_socket as c_cassini_spy_server_client_socket) do
begin m_cassini_port:= m_cassini_port;
m_on_display_cassini_spy_event:= m_on_display_cassini_spy_event; end;
// -- feed back to the caller
if Assigned(m_on_accept_tcp_cassini_spy_socket)
then m_on_accept_tcp_cassini_spy_socket(Self);
end; // handle_accept |
3.5 - The main form The main form simply
- creates the Spy Server Socket:
var g_c_cassini_spy: c_cassini_spy= Nil;
procedure TForm1.go_Click(Sender: TObject);
begin // -- free any previous spy
g_c_cassini_spy.Free;
g_c_cassini_spy:= c_cassini_spy.create_cassini_spy('cassini_spy',
k_site_path, StrToInt(cassini_port_.Text));
with g_c_cassini_spy do begin
m_trace_cassini_spy:= True;
start_cassini_spy(StrToInt(ie_port_.Text));
m_on_accept_tcp_cassini_spy_socket:= handle_tcp_cassini_spy_accept;
m_on_display_cassini_spy_event:= handle_cassini_spy_display; end;
end; // go_pane_lClick | - and handle the callback to display the packets in a tMemo
procedure TForm1.handle_cassini_spy_display(p_code: Integer; p_text: String);
// -- 1 from IE, 2 from CASSINI
var l_spaces: String;
procedure do_add(p_text: String);
begin
content_memo_.Lines.Add(l_spaces+ p_text);
raw_content_memo_.Lines.Add(p_text);
end; // do_add
var l_index: Integer;
l_the_line: String;
begin // handle_cassini_spy_display
case p_code of 1 : begin
l_spaces:= '';
do_add('');
do_add(IntToStr(g_request_count)+ ' ================');
Inc(g_request_count);
l_spaces:= ' -> | ';
end; 2 : begin
l_spaces:= '';
do_add('');
do_add('--------------------------------------');
l_spaces:= ' <- | ';
end;
else l_spaces:= '??';
end; // case l_the_line:= '';
l_index:= 1;
while l_index<= Length(p_text) do
if p_text[l_index]= k_return
then begin
do_add(l_the_line);
l_the_line:= '';
Inc(l_index, 2);
end else begin
l_the_line:= l_the_line+ p_text[l_index];
Inc(l_index); end;
// -- flush if l_the_line<> ''
then do_add(l_the_line);
end; // handle_cassini_spy_display |
4 - Mini How To
4.1 - The .aspx application The goal is to display the result of a simple multiplication. We build an .aspx page, with a simple calculator: - 2 TextBoxes where the user enters 2 values, a "calculate" Buttons which
submits the input values, and a result TextBox.
- the Button OnClick naturally computes the product
We explained elsewhere how to prepare the .aspx page. Nevertheless, without all
the screen snapshots, here is how to proceed: | start Delphi, select "New | ASP.NET Web application - Delphi for .NET" |
| using "File | Save as" (or the project manager) rename the page "a_01_calculator" | |
Delphi asks where to put the files and what the project name is | | enter the names. for instance:
and click "Ok" | |
drop the 3 TextBox from the "Web Controls" tab of the Palette on the Form Drop a Button, and create its Click handler. Compute the result of the multiplication in the handler. For instance:
procedure TWebForm1.Button1_Click(sender: System.Object; e: System.EventArgs);
var l_total: Integer; begin
l_total:= Convert.ToInt32(Textbox1.Text)* Convert.ToInt32(TextBox2.Text);
TextBox3.Text:= Convert.ToString(l_total);
end; | | Here is the content of the .aspx file (we added labels etc):
<%@ Page Language="c#" Debug="true"
Codebehind="a_01_calculator.pas" AutoEventWireup="false" Inherits="a_01_calculator.TWebForm1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head>
<title></title> </head>
<body ms_positioning="GridLayout"> <form runat="server">
<asp:TextBox id="TextBox1" style="Z-INDEX: 1; LEFT: 54px; POSITION: absolute; TOP: 62px"
runat="server" width="67px"></asp:TextBox>
<asp:TextBox id="TextBox2" style="Z-INDEX: 2; LEFT: 142px; POSITION: absolute; TOP: 62px"
runat="server" width="75px"></asp:TextBox>
<asp:TextBox id="TextBox3" style="Z-INDEX: 3; LEFT: 238px; POSITION: absolute; TOP: 62px"
runat="server" width="91px"></asp:TextBox>
<asp:Button id="Button1" style="Z-INDEX: 4; LEFT: 54px; POSITION: absolute; TOP: 102px"
runat="server" text="multiply"></asp:Button>
<asp:Label id="Label1" style="Z-INDEX: 5; LEFT: 54px; POSITION: absolute; TOP: 22px"
runat="server">price</asp:Label> <asp:Label id="Label2"
style="Z-INDEX: 6; LEFT: 126px; POSITION: absolute; TOP: 22px"
runat="server" width="3px">x</asp:Label>
<asp:Label id="Label3" style="Z-INDEX: 7; LEFT: 150px; POSITION: absolute; TOP: 22px"
runat="server">quantity</asp:Label> <asp:Label id="Label4"
style="Z-INDEX: 8; LEFT: 222px; POSITION: absolute; TOP: 22px"
runat="server">=</asp:Label> </form>
</body> </html> |
Then
Thats the standard Delphi way of building ASP.NET applications. But what was exactly exchanged between Internet Explorer and Delphi during those steps ? Well, enters the CASSINI SPY !
4.2 - Using the CASSINI SPY After the creation of the .ASPX and .PAS, there are 3 steps: - start CASSINI
- start the spy
- start IE and request the page
Start CASSINI:
| manually start up CASSINI by clicking on CassiniWebServer.Exe | |
CASSINI presents a parameter dialog | |
enter your application path, the port and the virtual path: and click "Start" | |
CASSINI is ready | | click "Start" |
| CASSINI presents all the files in our development folder: |
Start our CASSINI SPY
Start Internet Explorer: | start IE and enter the URL of our page (on port 81):
| | hit Enter | |
the CASSINI SPY displays the IE -> CASSINI packet as well as the answer: |
By copying the CASSINI SPY result to the ClipBoard, here is the full packet exchange: 0 ====================================== 405
-> | GET /_01_calculator/a_01_calculator.aspx HTTP/1.1 -> | Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, */*
-> | Accept-Language: fr -> | Accept-Encoding: gzip, deflate -> | User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows
NT 5.1; SV1; .NET CLR 1.1.4322) -> | Host: localhost:81 -> | Connection: Keep-Alive
-> | Cookie: Name=the_cookie; ASP.NET_SessionId=hlmd5ti4pfwssz452iog15uh -> | -> | -------------------------------------- 1533 <- | HTTP/1.1 200 OK <- | Server: Cassini/1.0.0.0
<- | Date: Thu, 16 Feb 2006 18:02:04 GMT <- | X-AspNet-Version: 1.1.4322 <- | Cache-Control: private <- | Content-Type: text/html; charset=utf-8 <- | Content-Length: 1318 <- | Connection: Close
<- | <- | <- | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <- | <- | <html> <- | <head> <- | <title></title>
<- | </head> <- | <body ms_positioning="GridLayout"> <- | <form name="_ctl0" method="post" action="a_01_calculator.aspx" id="_ctl0"> <- | <input type="hidden" name="__VIEWSTATE"
value="dDwtMTcyMDIwMDYzNzs7PnErGOMElmmKP3tC3Yl1ObO7Bo1L" /> <- | <- | <input name="TextBox1" type="text" id="TextBox1"
style="width:67px;Z-INDEX: 1; LEFT: 54px; POSITION: absolute; TOP: 62px" /> <- | <input name="TextBox2" type="text" id="TextBox2"
style="width:75px;Z-INDEX: 2; LEFT: 142px; POSITION: absolute; TOP: 62px" /> <- | <input name="TextBox3" type="text" id="TextBox3"
style="width:91px;Z-INDEX: 3; LEFT: 238px; POSITION: absolute; TOP: 62px" /> <- | <input type="submit" name="Button1" value="multiply" id="Button1"
style="Z-INDEX: 4; LEFT: 54px; POSITION: absolute; TOP: 102px" /> <- | <span id="Label1" style="Z-INDEX: 5; LEFT: 54px; POSITION:
absolute; TOP: 22px"> price</span> <- | <span id="Label2" style="width:3px;Z-INDEX: 6; LEFT: 126px; POSITION: absolute; TOP: 22px">
x</span> <- | <span id="Label3" style="Z-INDEX: 7; LEFT: 150px; POSITION: absolute; TOP: 22px">
quantity</span> <- | <span id="Label4" style="Z-INDEX: 8; LEFT: 222px; POSITION: absolute; TOP: 22px">
=</span> <- | </form> <- | </body> <- | </html> <- | |
And here is the result of sending over 15 x 3 and clicking "multiply" : 1 ====================================== 677
-> | POST /_01_calculator/a_01_calculator.aspx HTTP/1.1 -> | Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, */*
-> | Referer: http://localhost:81/_01_calculator/a_01_calculator.aspx -> | Accept-Language: fr -> | Content-Type: application/x-www-form-urlencoded -> | Accept-Encoding: gzip, deflate
-> | User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322) -> | Host: localhost:81 -> | Content-Length: 110
-> | Connection: Keep-Alive -> | Cache-Control: no-cache -> | Cookie: Name=the_cookie; ASP.NET_SessionId=hlmd5ti4pfwssz452iog15uh -> | -> | __VIEWSTATE=dDwtMTcyMDIwMDYzNzs7PnErGOMElmmKP3tC3Yl1ObO7Bo1L
&TextBox1=15 &TextBox2=3
&TextBox3= &Button1=multiply --------------------------------------215
<- | HTTP/1.1 200 OK <- | Server: Cassini/1.0.0.0 <- | Date: Thu, 16 Feb 2006 18:08:10 GMT <- | X-AspNet-Version: 1.1.4322 <- | Cache-Control: private <- | Content-Type: text/html; charset=utf-8
<- | Content-Length: 1350 <- | Connection: Close <- | <- | --------------------------------------1351 <- | <- | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<- | <- | <html> <- | <head> <- | <title></title> <- | </head> <- | <body ms_positioning="GridLayout">
<- | <form name="_ctl0" method="post" action="a_01_calculator.aspx" id="_ctl0"> <- | <input type="hidden" name="__VIEWSTATE"
value="dDwtMTcyMDIwMDYzNzs7PnErGOMElmmKP3tC3Yl1ObO7Bo1L" /> <- | <- | <input name="TextBox1" type="text" value="15" id="TextBox1"
style="width:67px;Z-INDEX: 1; LEFT: 54px; POSITION: absolute; TOP: 62px" /> <- | <input name="TextBox2" type="text" value="3" id="TextBox2"
style="width:75px;Z-INDEX: 2; LEFT: 142px; POSITION: absolute; TOP: 62px" /> <- | <input name="TextBox3" type="text" value="45" id="TextBox3"
style="width:91px;Z-INDEX: 3; LEFT: 238px; POSITION: absolute; TOP: 62px" /> <- | <input type="submit" name="Button1" value="multiply"
id="Button1" style="Z-INDEX: 4; LEFT: 54px; POSITION: absolute; TOP: 102px" /> <- | <span id="Label1" style="Z-INDEX: 5; LEFT: 54px;
POSITION: absolute; TOP: 22px">price</span> <- | <span id="Label2" style="width:3px;Z-INDEX: 6; LEFT: 126px;
POSITION: absolute; TOP: 22px">x</span> <- | <span id="Label3" style="Z-INDEX: 7; LEFT: 150px;
POSITION: absolute; TOP: 22px">quantity</span> <- | <span id="Label4" style="Z-INDEX: 8; LEFT: 222px;
POSITION: absolute; TOP: 22px">=</span> <- | </form> <- | </body> <- | </html> <- | |
Of particular interest are - the ViewState value
- the fact that IE only sent back the values of the controls (this is also the case for CGI)
- the labels are ALSO sent back (I believed only the values of the controls were sent back)
5 - Improvements This utility was built in on day. We have the tool, and will use it in the our
forthcoming ASP.NET tutorials. To change this into an "industrial strength" tool would require the addition of some automatic invocation: when the developper clicks "Run", we should force
Delphi to launch our tool, and the SPY in turn should start CASSINI.
6 - Download the Sources Here are the source code files: The .ZIP file(s) contain: - the main program (.DPR, .DOF, .RES), the main form (.PAS, .DFM), and any other auxiliary form
- any .TXT for parameters, samples, test data
- all units (.PAS) for units
Those .ZIP - are self-contained: you will not need any other product (unless expressly mentioned).
- for Delphi 6 projects, can be used from any folder (the pathes are RELATIVE)
- will not modify your PC in any way beyond the path where you placed the .ZIP (no registry changes, no path creation etc).
To use the .ZIP:
- create or select any folder of your choice
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder.
The Pascal code uses the Alsacian notation, which prefixes identifier by program area: K_onstant, T_ype, G_lobal, L_ocal, P_arametre, F_unction, C_lasse etc. This notation is presented in the Alsacian Notation paper.
As usual:
- please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs, broken links or had some problem downloading the file. Resulting corrections will
be helpful for other readers
- we welcome any comment, criticism, enhancement, other sources or reference suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
- or more simply, enter your (anonymous or with your e-mail if you want an answer) comments below and clic the "send" button
- and if you liked this article, talk about this site to your fellow developpers, add a link to your links page ou mention our articles in your blog or newsgroup posts when relevant. That's the way we operate:
the more traffic and Google references we get, the more articles we will write.
7 - References You may look at:
8 - The author Felix John COLIBRI works at the Pascal Institute. Starting with Pascal in 1979, he then became involved with Object
Oriented Programming, Delphi, Sql, Tcp/Ip, Html, UML. Currently, he is mainly active in the area of custom software
development (new projects, maintenance, audits, BDE migration, Delphi Xe_n migrations, refactoring), Delphi Consulting and Delph training. His web site features tutorials, technical papers about programming with full downloadable source
code, and the description and calendar of forthcoming Delphi, FireBird, Tcp/IP, Web Services, OOP / UML, Design Patterns, Unit Testing training sessions.
|